{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5d84ff9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | default_exp _components.docs_dependencies"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b4b0e6c8",
   "metadata": {},
   "source": [
    "# Install docs dependencies"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16950f9d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "import asyncio\n",
    "import os\n",
    "import platform\n",
    "import shutil\n",
    "import subprocess  # nosec Issue: [B404:blacklist]\n",
    "import tarfile\n",
    "import zipfile\n",
    "from pathlib import Path\n",
    "from tempfile import TemporaryDirectory\n",
    "\n",
    "from fastkafka._components.helpers import in_notebook\n",
    "from fastkafka._components.logger import get_logger\n",
    "\n",
    "if in_notebook():\n",
    "    from tqdm.notebook import tqdm\n",
    "else:\n",
    "    from tqdm import tqdm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2f57c397",
   "metadata": {},
   "outputs": [],
   "source": [
    "from contextlib import contextmanager\n",
    "\n",
    "import pytest"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "991a4c6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "logger = get_logger(__name__)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7637df53",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "npm_required_major_version = 9\n",
    "\n",
    "\n",
    "def _check_npm(required_major_version: int = npm_required_major_version) -> None:\n",
    "    \"\"\"\n",
    "    Check if npm is installed and its major version is compatible with the required version.\n",
    "\n",
    "    Args:\n",
    "        required_major_version: Required major version of npm. Defaults to 9.\n",
    "\n",
    "    Raises:\n",
    "        RuntimeError: If npm is not found or its major version is lower than the required version.\n",
    "    \"\"\"\n",
    "    if shutil.which(\"npm\") is not None:\n",
    "        cmd = \"npm --version\"\n",
    "        proc = subprocess.run(  # nosec [B602:subprocess_popen_with_shell_equals_true]\n",
    "            cmd,\n",
    "            shell=True,\n",
    "            check=True,\n",
    "            capture_output=True,\n",
    "        )\n",
    "        major_version = int(proc.stdout.decode(\"UTF-8\").split(\".\")[0])\n",
    "        if major_version < required_major_version:\n",
    "            raise RuntimeError(\n",
    "                f\"Found installed npm major version: {major_version}, required npx major version: {required_major_version}. To use documentation features of FastKafka, please update npm\"\n",
    "            )\n",
    "    else:\n",
    "        raise RuntimeError(\n",
    "            f\"npm not found, to use documentation generation features of FastKafka, you must have npm >= {required_major_version} installed\"\n",
    "        )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fe7da20a",
   "metadata": {},
   "outputs": [],
   "source": [
    "@contextmanager\n",
    "def _clean_path():\n",
    "    path = os.environ[\"PATH\"]\n",
    "    try:\n",
    "        os.environ[\"PATH\"] = \"\"\n",
    "        yield\n",
    "    finally:\n",
    "        os.environ[\"PATH\"] = path"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f37a6e80",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | notest\n",
    "\n",
    "_check_npm()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "363f428b",
   "metadata": {},
   "outputs": [],
   "source": [
    "with _clean_path():\n",
    "    with pytest.raises(RuntimeError) as e:\n",
    "        await _check_npm()\n",
    "\n",
    "assert (\n",
    "    e.value.args[0]\n",
    "    == \"npm not found, to use documentation generation features of FastKafka, you must have npm >= 9 installed\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "996288e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "with pytest.raises(RuntimeError) as e:\n",
    "    await _check_npm(required_major_version=999)\n",
    "\n",
    "assert (\n",
    "    e.value.args[0]\n",
    "    == \"Found installed npm major version: 9, required npx major version: 999. To use documentation features of FastKafka, please update npm\"\n",
    "), e.value.args[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a015d2c2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "node_version = \"v18.15.0\"\n",
    "node_fname_suffix = \"win-x64\" if platform.system() == \"Windows\" else \"linux-x64\"\n",
    "node_fname = f\"node-{node_version}-{node_fname_suffix}\"\n",
    "node_fname_extension = \".zip\" if platform.system() == \"Windows\" else \".tar.xz\"\n",
    "node_url = f\"https://nodejs.org/dist/{node_version}/{node_fname}{node_fname_extension}\"\n",
    "local_path = (\n",
    "    Path(os.path.expanduser(\"~\")).parent / \"Public\"\n",
    "    if platform.system() == \"Windows\"\n",
    "    else Path(os.path.expanduser(\"~\")) / \".local\"\n",
    ")\n",
    "tgz_path = local_path / f\"{node_fname}{node_fname_extension}\"\n",
    "node_path = local_path / f\"{node_fname}\"\n",
    "\n",
    "\n",
    "def _check_npm_with_local(node_path: Path = node_path) -> None:\n",
    "    \"\"\"\n",
    "    Check if npm is installed and its major version is compatible with the required version.\n",
    "    If npm is not found but a local installation of NodeJS is available, add the NodeJS binary path to the system's PATH environment variable.\n",
    "\n",
    "    Args:\n",
    "        node_path: Path to the local installation of NodeJS. Defaults to node_path.\n",
    "\n",
    "    Raises:\n",
    "        RuntimeError: If npm is not found and a local installation of NodeJS is not available.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        _check_npm()\n",
    "    except RuntimeError as e:\n",
    "        if (node_path).exists():\n",
    "            logger.info(\"Found local installation of NodeJS.\")\n",
    "            node_binary_path = (\n",
    "                f\";{node_path}\"\n",
    "                if platform.system() == \"Windows\"\n",
    "                else f\":{node_path / 'bin'}\"\n",
    "            )\n",
    "            os.environ[\"PATH\"] = os.environ[\"PATH\"] + node_binary_path\n",
    "            _check_npm()\n",
    "        else:\n",
    "            raise e"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d8c5b65",
   "metadata": {},
   "outputs": [],
   "source": [
    "_check_npm_with_local()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86b77faf",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | notest\n",
    "# Not reentrant because when local is installed, it will break\n",
    "\n",
    "with _clean_path():\n",
    "    with pytest.raises(RuntimeError) as e:\n",
    "        _check_npm_with_local()\n",
    "\n",
    "assert (\n",
    "    e.value.args[0]\n",
    "    == \"npm not found, to use documentation generation features of FastKafka, you must have npm >= 9 installed\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8e6f1782",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "\n",
    "def _install_node(\n",
    "    *,\n",
    "    node_url: str = node_url,\n",
    "    local_path: Path = local_path,\n",
    "    tgz_path: Path = tgz_path,\n",
    ") -> None:\n",
    "    \"\"\"\n",
    "    Install NodeJS by downloading the NodeJS distribution archive, extracting it, and adding the NodeJS binary path to the system's PATH environment variable.\n",
    "\n",
    "    Args:\n",
    "        node_url: URL of the NodeJS distribution archive to download. Defaults to node_url.\n",
    "        local_path: Path to store the downloaded distribution archive. Defaults to local_path.\n",
    "        tgz_path: Path of the downloaded distribution archive. Defaults to tgz_path.\n",
    "    \"\"\"\n",
    "    try:\n",
    "        import requests\n",
    "    except Exception as e:\n",
    "        msg = \"Please install docs version of fastkafka using 'pip install fastkafka[docs]' command\"\n",
    "        logger.error(msg)\n",
    "        raise RuntimeError(msg)\n",
    "\n",
    "    logger.info(\"Installing NodeJS...\")\n",
    "    local_path.mkdir(exist_ok=True, parents=True)\n",
    "    response = requests.get(\n",
    "        node_url,\n",
    "        stream=True,\n",
    "        timeout=60,\n",
    "    )\n",
    "    try:\n",
    "        total = response.raw.length_remaining // 128\n",
    "    except Exception:\n",
    "        total = None\n",
    "\n",
    "    with open(tgz_path, \"wb\") as f:\n",
    "        for data in tqdm(response.iter_content(chunk_size=128), total=total):\n",
    "            f.write(data)\n",
    "\n",
    "    if platform.system() == \"Windows\":\n",
    "        with zipfile.ZipFile(tgz_path, \"r\") as zip_ref:\n",
    "            zip_ref.extractall(\n",
    "                local_path\n",
    "            )  # nosec: B202 tarfile_unsafe_members - tarfile.extractall used without any validation. Please check and discard dangerous members.\n",
    "    else:\n",
    "        with tarfile.open(tgz_path) as tar:\n",
    "            for tarinfo in tar:\n",
    "                tar.extract(tarinfo, local_path)\n",
    "\n",
    "    os.environ[\"PATH\"] = (\n",
    "        os.environ[\"PATH\"] + f\";{node_path}\"\n",
    "        if platform.system() == \"Windows\"\n",
    "        else f\":{node_path}/bin\"\n",
    "    )\n",
    "    logger.info(f\"Node installed in {node_path}.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "55a078fe",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[INFO] __main__: Installing NodeJS...\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "5a68aefa6df6433ea191b07606462cca",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "  0%|          | 0/184668 [00:00<?, ?it/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[INFO] __main__: Node installed in /home/davor/.local/node-v18.15.0-linux-x64.\n"
     ]
    }
   ],
   "source": [
    "# | notest\n",
    "# Breaks because other tests running in parallel are using already installed node\n",
    "\n",
    "_install_node()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9595e0bb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[INFO] __main__: Found local installation of NodeJS.\n"
     ]
    }
   ],
   "source": [
    "# | notest\n",
    "\n",
    "with _clean_path():\n",
    "    _check_npm_with_local()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "51056070",
   "metadata": {},
   "outputs": [],
   "source": [
    "# | export\n",
    "\n",
    "\n",
    "async def _install_docs_npm_deps() -> None:\n",
    "    \"\"\"\n",
    "    Install the required npm dependencies for generating the documentation using AsyncAPI generator.\n",
    "    \"\"\"\n",
    "    with TemporaryDirectory() as d:\n",
    "        cmd = (\n",
    "            \"npx -y -p @asyncapi/generator ag https://raw.githubusercontent.com/asyncapi/asyncapi/master/examples/simple.yml @asyncapi/html-template -o \"\n",
    "            + d\n",
    "        )\n",
    "\n",
    "        proc = await asyncio.create_subprocess_shell(\n",
    "            cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE\n",
    "        )\n",
    "        stdout, stderr = await proc.communicate()\n",
    "\n",
    "        if proc.returncode == 0:\n",
    "            logger.info(\"AsyncAPI generator installed\")\n",
    "        else:\n",
    "            logger.error(\"AsyncAPI generator NOT installed!\")\n",
    "            logger.info(\n",
    "                f\"stdout of '$ {cmd}'{stdout.decode('UTF-8')} \\n return_code={proc.returncode}\"\n",
    "            )\n",
    "            logger.info(\n",
    "                f\"stderr of '$ {cmd}'{stderr.decode('UTF-8')} \\n return_code={proc.returncode}\"\n",
    "            )\n",
    "            raise ValueError(\n",
    "                f\"\"\"AsyncAPI generator NOT installed, used '$ {cmd}'\n",
    "----------------------------------------\n",
    "stdout:\n",
    "{stdout.decode(\"UTF-8\")}\n",
    "----------------------------------------\n",
    "stderr:\n",
    "{stderr.decode(\"UTF-8\")}\n",
    "----------------------------------------\n",
    "return_code={proc.returncode}\"\"\"\n",
    "            )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2a498aa8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[INFO] __main__: AsyncAPI generator installed\n"
     ]
    }
   ],
   "source": [
    "# | notest\n",
    "# Breaks because other tests running in parallel are using already installed node\n",
    "\n",
    "await _install_docs_npm_deps()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7aae8f4f",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "python3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
